# $Id: c2c.tcl 31 2007-08-25 00:50:31Z khomoutov $
# Client-to-client part of DC.

namespace eval ::dcclient::c2c {
	namespace import ::dccore::*

	hook new c2c_connected
	hook new c2c_disconnected
	hook new c2c_handshake_failure
	hook add c2c_in_command [mycmd ProcessCommand] -iscommand -prio 100
	hook add c2c_in_sr_raw [mycmd ParseCmdSearchReply] -iscommand -prio 100
	hook add c2c_in_cmd_get [mycmd ProcessCmdGet] -iscommand -prio 100
}

# args: same as of ::dccore::connect, plus
#  -mynick NICK -- report user's nick as NICK;
#  -direction DIR -- select upload or download as the transfer direction.
proc ::dcclient::c2c::c2c_connect args {
	variable peers
	variable peerid

	if {![info exists peerid]} { set peerid 0 }
	set peer peer$peerid

	set direction dontcare
	set encoding [encoding system]

	set cmd [list connect]

	foreach {opt val} $args {
		switch -- $opt {
			-mynick { set mynick $val }
			-direction { set direction $val }
			-encoding { set encoding $val }
			default { lappend cmd $opt $val }
		}
	}

	assert_required_args_present mynick -mynick

	set sock [eval $cmd -encoding binary]

	variable $peer
	upvar 0 $peer state
	set state(sock) $sock

	set state(mynick) $mynick
	set state(encoding) $encoding
	set state(direction) $direction

	hook run c2c_connected $peer

	debugmsg peer "$peer: logging in"

	set state(mylock) dcclient
	# send initial stanzas:
	set nick [encoding convertto $encoding $mynick]
	puts -nonewline $sock "\$MyNick $nick|\$Lock $state(mylock) Pk=none|"

	set state(mode)  command
	set state(commands) {$MyNick}

	fileevent $sock readable \
		[mycmd ProcessInDataChunk $sock $peer]

	Expect $peer \$MyNick [mycmd OnPeerMyNick]

	vwait [my ${peer}(result)]

	debugmsg peer "$peer: $result"

	lassign $result code msg
	switch -- $code {
		OK {
			lappend peers $peer
			incr peerid
			return $peer
		}
		ERROR {
			return -code error $msg
		}
		default {
			error "Unknown login result: $result"
		}
	}
}

proc ::dcclient::c2c::ProcessInDataChunk {sock peer} {
	debugmsg peer [info level 0]

	if {[eof $sock]} {
		c2c_disconnect $peer
		return
	}

	variable $peer
	upvar 0 $peer state

	switch -- $state(mode) {
		command {
			upvar 0 state(acc) acc
			append acc [read $sock]
			set from 0; set dp 0
			while 1 {
				set dp [string first | $acc $dp]
				if {$dp >= 0} {
					set stanza [string range $acc $from [expr {$dp - 1}]]
					hook run c2c_in_command $peer #[info level] stanza
					incr dp; set from $dp
				} else {
					set acc [string range $acc $from end]
					break
				}
			}
		}
		data {
			hook run c2c_in_data $peer [read $sock]
		}
	}
}

proc ::dcclient::c2c::Expect {peer args} {
	variable $peer
	upvar 0 $peer state

	if {[llength $args] == 0} {
		set out [list]
		foreach token [array names state expected,*] {
			lappend out [string range $token \
				[expr {[string first , $token] + 1}] end] \
				$state($token)
		}
		return $out
	}

	array unset state expected,*

	foreach {cmd script} $args {
		set state(expected,$cmd) $script
	}
}

proc ::dcclient::c2c::ProcessExpected {peer cmd level args} {
	variable $peer
	upvar 0 $peer state

	#debugmsg peer [info level 0]
	#parray state

	set expected [info exists state(expected,$cmd)]
	if {$expected} {
		set script $state(expected,$cmd) 
		array unset state expected,*
		eval [list $script $peer $level] $args
	}
	
	set expected
}

proc ::dcclient::c2c::ProcessCommand {peer level vstanza} {
	variable $peer
	upvar 0 $peer state
	upvar $level $vstanza stanza

	::dccore::ParseStanza $stanza cmd param

	switch -- $cmd {
		$MyNick {
			set nick [encoding convertfrom $state(encoding) $param]
			set arglist nick
		}
		$Lock {
			lassign [split $param] lock pk
			set pkey [lindex [split $pk =] 1]
			set arglist {lock pkey}
		}
		$Key -
		$Error -
		$ListLen -
		$FileLength {
			set arglist param
		}
		$Get {
			lassign [split $param $] fname from
			set arglist {fname from}
		}
		$Supports {
			set caps [split [string trim $param]]
			set arglist caps
		}
		$Direction {
			lassign [split $param] dir prio
			set arglist {dir prio}
		}
		$GetListLen -
		$Canceled {
			set arglist ""
		}
		$ADCGET -
		$ADCSND {
			lassign [split $param] type what from len
			set arglist {type what from len}
		}
		default {
			hook run c2c_in_unknown_cmd $peer #[info level] cmd param
			return
		}
	}

	eval [list ProcessExpected $peer $cmd #[info level]] $arglist

	eval [list hook run \
		c2c_in_cmd_[string tolower [string range $cmd 1 end]] \
		$peer #[info level]] $arglist
}

proc ::dcclient::c2c::OnPeerMyNick {peer level vnick} {
	variable $peer
	upvar 0 $peer state
	upvar $level $vnick nick

	set state(peernick) $nick
	debugmsg peer "$peer: Got nick"

	Expect $peer \$Lock [mycmd OnPeerLock]
}

proc ::dcclient::c2c::OnPeerLock {peer level vlock vpkey} {
	debugmsg peer "$peer: Got lock"

	variable $peer
	upvar 0 $peer state
	upvar $level $vlock lock
	set state(peerlock) $lock

	set state(peerextended) [string match EXTENDEDPROTOCOL* $lock]
	debugmsg peer "$peer: EXT? $state(peerextended)"

	Expect $peer \
		\$Direction [mycmd OnPeerDirection] \
		\$Supports  [mycmd OnPeerSupports]
}

proc ::dcclient::c2c::OnPeerSupports {peer level vcaps} {
	upvar $level $vcaps caps

	debugmsg peer "$peer: supports: $caps"

	Expect $peer \$Direction [mycmd OnPeerDirection]
}

proc ::dcclient::c2c::OnPeerDirection {peer level vdir vprio} {
	debugmsg peer "$peer: Got direction"

	variable $peer
	upvar 0 $peer state
	upvar $level $vdir dir $vprio prio

	# TODO check parameters, report protocol error, if bad:
	set state(peerdirection) [string tolower $dir]
	set state(peerprio) $prio

	Expect $peer \$Key [mycmd OnPeerKey]
}

proc ::dcclient::c2c::OnPeerKey {peer level vkey} {
	debugmsg peer "$peer: Got peer key"

	variable $peer
	upvar 0 $peer state

	# Just playing, not really needed:
	upvar $level $vkey key
	set mykey [::dccore::BinaryGenKey $state(mylock)]
	if {![string equal $mykey $key]} {
		return -code error "Keys mismatch"
	}

	set key [::dccore::BinaryGenKey $state(peerlock)]

	if {[string equal $state(direction) dontcare]} {
		switch -- $state(peerdirection) {
			upload   { set mydir Download }
			download { set mydir Upload }
		}
		set myprio [expr {$state(peerprio) - 1}]
	} else {
		set mydir [string toupper $state(direction) 0 0]
		# TODO implement more serious approach:
		set myprio [expr {int(rand() * 100000)}]
	}

	puts -nonewline $state(sock) \
		"\$Direction $mydir $myprio|\$Key $key|"

	debugmsg peer "$peer: handshake completed"
}

proc ::dcclient::c2c::c2c_disconnect peer {
	variable $peer
	upvar 0 $peer state

	close $state(sock)
	unset state
}

proc ::dcclient::c2c::OnMyNick {peer level vnick} {
	variable $peer
	upvar 0 $peer state
	upvar $level $vnick nick

	set state(peernick) $nick

	debugmsg peer "$peer: \$MyNick: $nick|"
}

proc ::dcclient::c2c::ProcessCmdGet {peer level vfile vfrom} {
	upvar $level $vfile file $vfrom from

	debugmsg peer "$peer: \$Get file: $file| from: $from|"
}

proc ::dcclient::search args {
	set myport 0
	set mediatype any
	set mode active

	set cmd [SRListenerOpen]

	foreach {opt val} $args {
		switch -- $opt {
			-hub { set hub $val }
			-myaddr { set myaddr $val }
			-myport { set myport $val }
			-mediatype -
			-type { set mediatype $val }
			-keywords { set keywords $val }
			-mode { set mode $val }
			default {
				return -code error "bad option \"$opt\":\
					must be one of -hub, -myaddr, -myport,\
					-mediatype, -keywords or -mode"
			}
		}
	}

	assert_required_args_present hub -hub mediatype -mediatype keywords -keywords

	if {[info exists myaddr]} { lappend cmd -myaddr $myaddr }
	if {[info exists myport]} { lappend cmd -myaddr $myport }
	set sock [eval $cmd]
}

proc ::dcclient::SRListenerOpen args {
}

proc ::dcclient::ParseCmdSearchReply args {
}

